/* @flow */

import VNode from "./vnode";
import {
  resolveConstructorOptions
} from "core/instance/init";
import {
  queueActivatedComponent
} from "core/observer/scheduler";
import {
  createFunctionalComponent
} from "./create-functional-component";

import {
  warn,
  isDef,
  isUndef,
  isTrue,
  isObject
} from "../util/index";

import {
  resolveAsyncComponent,
  createAsyncPlaceholder,
  extractPropsFromVNodeData,
} from "./helpers/index";

import {
  callHook,
  activeInstance,
  updateChildComponent,
  activateChildComponent,
  deactivateChildComponent,
} from "../instance/lifecycle";

import {
  isRecyclableComponent,
  renderRecyclableComponentTemplate,
} from "weex/runtime/recycle-list/render-component-template";

// inline hooks to be invoked on component VNodes during patch
// 组件内部自带钩子
const componentVNodeHooks = {
  init(vnode: VNodeWithData, hydrating: boolean): ? boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode; // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode);
    } else {
      const child = (vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      ));
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },

  prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    const options = vnode.componentOptions;
    const child = (vnode.componentInstance = oldVnode.componentInstance);
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  },

  insert(vnode: MountedComponentVNode) {
    const {
      context,
      componentInstance
    } = vnode;
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true;
      callHook(componentInstance, "mounted");
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance);
      } else {
        activateChildComponent(componentInstance, true /* direct */ );
      }
    }
  },

  destroy(vnode: MountedComponentVNode) {
    const {
      componentInstance
    } = vnode;
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy();
      } else {
        deactivateChildComponent(componentInstance, true /* direct */ );
      }
    }
  },
};

const hooksToMerge = Object.keys(componentVNodeHooks);
// 创建子组件过程
export function createComponent(
  Ctor: Class < Component > | Function | Object | void, // 子类构造器
  data: ? VNodeData,
  context : Component, // vm实例
  children: ? Array < VNode > , // 子节点
  tag ? : string // 子组件占位符
): VNode | Array < VNode > | void {
  if (isUndef(Ctor)) {
    return;
  }
  // Vue.options里的_base属性存储Vue构造器
  const baseCtor = context.$options._base;
  // 针对局部组件注册场景
  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    //局部注册
    Ctor = baseCtor.extend(Ctor);
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== "function") {
    if (process.env.NODE_ENV !== "production") {
      warn(`Invalid Component definition: ${String(Ctor)}`, context);
    }
    return;
  }

  // async component
  let asyncFactory;
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor;
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor);
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(asyncFactory, data, context, children, tag);
    }
  }

  data = data || {};

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  // 构造器配置合并
  resolveConstructorOptions(Ctor);

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props props校验
  const propsData = extractPropsFromVNodeData(data, Ctor, tag);

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children);
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners

  /**
   * 有了render函数接下来会根据它创建Vnode实例，其中遇到组件占位符节点时会创建子组件Vnode，
   *  此时为on,nativeOn做了一层特殊的转换，将nativeOn赋值给on,这样后续的处理方式和普通节点一致。
   * 另外，将on赋值给listeners,在创建VNode时以组件配置componentOptions传入。
   *
   */
  //这说明所有的原生浏览器事件处理是在当前父组件环境中处理的。
  //而对于自定义事件，会把 listeners 作为 vnode 的 componentOptions 传入，放在子组件初始化阶段中处理，
  //在子组件的初始化的时候， 拿到了父组件传入的 listeners，然后在执行 initEvents 的过程中，会处理这个 listeners。
  //data.on和data.nativeOn用于子组件时的区分。.native是事件带native标识符。表示原生事件。不带。native代表自定义事件
  const listeners = data.on; //把 自定义事件data.on 赋值给了 listeners
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn; //把浏览器原生事件 data.nativeOn 赋值给了 data.on，变成正常事件的处理

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot;
    data = {};
    if (slot) {
      data.slot = slot;
    }
  }

  // install component management hooks onto the placeholder node
  // 挂载组件钩子
  //// 将componentVNodeHooks 钩子函数合并到组件data.hook中
  installComponentHooks(data);

  // return a placeholder vnode
  // 创建子组件vnode，名称以 vue-component- 开头
  const name = Ctor.options.name || tag;
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
    data,
    undefined,
    undefined,
    undefined,
    context, {
      Ctor,
      propsData,
      listeners,
      tag,
      children
    },
    asyncFactory
  );

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode);
  }

  return vnode;
}

export function createComponentInstanceForVnode(
  // we know it's MountedComponentVNode but flow doesn't
  vnode: any,
  // activeInstance in lifecycle state
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent,
  };
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnode.componentOptions.Ctor(options);
}

function installComponentHooks(data: VNodeData) {
  const hooks = data.hook || (data.hook = {});
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i];
    const existing = hooks[key];
    const toMerge = componentVNodeHooks[key];
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge;
    }
  }
}

function mergeHook(f1: any, f2: any): Function {
  const merged = (a, b) => {
    // flow complains about extra args which is why we use any
    f1(a, b);
    f2(a, b);
  };
  merged._merged = true;
  return merged;
}

// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel(options, data: any) {
  const prop = (options.model && options.model.prop) || "value";
  const event = (options.model && options.model.event) || "input";
  (data.attrs || (data.attrs = {}))[prop] = data.model.value;
  const on = data.on || (data.on = {});
  const existing = on[event];
  const callback = data.model.callback;
  if (isDef(existing)) {
    if (
      Array.isArray(existing) ?
      existing.indexOf(callback) === -1 :
      existing !== callback
    ) {
      on[event] = [callback].concat(existing);
    }
  } else {
    on[event] = callback;
  }
}
